# -*- coding: utf-8 -*-
#
# Copyright 2009 Vanderbilt University
# 
# Licensed under the Apache License, Version 2.0 (the "License"); 
# you may not use this file except in compliance with the License. 
# You may obtain a copy of the License at 
# 
#     http://www.apache.org/licenses/LICENSE-2.0 
# 
# Unless required by applicable law or agreed to in writing, software 
# distributed under the License is distributed on an "AS IS" BASIS, 
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
# See the License for the specific language governing permissions and 
# limitations under the License. 

"""Important core classes for the network. Due to high usage, this module
can be compiled into machine code with Cython for improved performance.

.. moduleauthor:: John Paulett <john.paulett -at- vanderbilt.edu>
"""

class EdgeDetail(object):
    """Structure that holds details about an edge, such as the similarity and 
    the size. EdgeDetail objects are not hashable, thus not suitable to be used
    as keys.
    
    Attributes:
    
        * *similarity*
        * *size* The number of items in common between the two nodes.
        * *joint* The joint probability of node A and node B, P(A,B) or
          P(A and B). Also known as the support of a rule.
        * *conditional* The conditional probability of node B give node A, 
          P(B|A). Also known as the confidence of a rule.
    
    These attributes default to 0.  They can optionally be set as keyed 
    arguments to the constructor:
    
    >>> d = EdgeDetail(size=450, joint=0.3, conditional=0.7)
    """
    def __init__(self, **kwargs):
        self.similarity = 0
        self.size = 0
        self.joint = 0
        self.conditional = 0
        
        if kwargs is not None:
           for k, v in kwargs.iteritems():
               self.__setattr__(k, v) 
    
    def items(self):
        return self.__dict__.items()
    
    def __repr__(self):
        return '<EdgeDetail(size=%i)>' % self.size
    
    def __eq__(self, other):
        try:
            return self.__dict__ == other.__dict__
        except:
            return False
    
    def __ne__(self, other):
        return not self.__eq__(other)
    

class Node(object):
    def __init__(self, id):
        self.id = id
        self.size = 0
        self.edges = []
        
    def __eq__(self, other):
        """Override to base object on the id attribute.
        
        >>> a,b = Node(12), Node(12)
        >>> a == b
        True
        """
        if other is None:
            return False
        return self.id == other.id
    
    def __ne__(self, other):
        """
        >>> a,b = Node(1), Node(2)
        >>> a != b
        True
        """
        return not self.__eq__(other)
    
    def __hash__(self):
        """Override to base object on the id attribute.
        
        >>> string = 'a string'
        >>> node = Node('a string')
        >>> hash(string) == hash(node)
        True
        """
        return self.id.__hash__()
    
    def __str__(self):
        return str(self.id)
    
    def __repr__(self):
        return '<Node(%s)>' % self.id
        
    def add_edge(self, edge):
        """Add an edge to this node.  If the graph is directed, pass in the 
        directed version of the edge.
        
        >>> n = Node('id')
        >>> n.edges
        []
        >>> n.add_edge((1, 2))
        >>> n.edges
        [(1, 2)]
        """
        self.edges.append(edge)
    
    def remove_edge(self, edge):
        """Remove an edge from this node.  If the graph is directed, pass in 
        the directed version of the edge.
        
        >>> n = Node('id')
        >>> n.edges = [(Node(1), n)]
        >>> n.remove_edge((Node(1), n))
        >>> n.edges
        []
        """
        self.edges.remove(edge)
        # FIXME may be inefficient, consider directly removing from list
        # or having calling method handle re-finalizing network with a 
        # no-op sort when that method is finished.
        #self._generate_friends()
    
    def finalize(self, sort):
        """Optimization that sorts the edges of the node. Must be called once 
        all the edges are set into the nodes.  Other methods assume that that 
        finalize has been called.  The object can be in an inconsistent state 
        of finalized is not called.
        
        sort - the function that returns a properly sorted list        
        """
        self.edges = sort(self.edges)
        #self._generate_friends()

    #def satisfied(self):
    #    """Returns boolean of whether or not this node is satisfied.
    #    Satisfaction is defined as the node having at least one reciprocal edge. 
    #    """
    #    # TODO we could replace this by
    #   for friend in self.__friends:
    #       if self in friend.friends:
    #            return True
    #    return False

    def other_node(self, edge):
        """Retrieve the node in the edge that is not this node.
        
        >>> n1, n2 = Node('1'), Node('2')
        >>> edge = (n1, n2)
        >>> n1.other_node(edge) == n2
        True
        >>> n2.other_node(edge) == n1
        True
        """
        if self is edge[0]:
            return edge[1]
        return edge[0]

    @property
    def friends(self):
        #return self.__friends
        return [self.other_node(edge) for edge in self.edges]
        
    #def _generate_friends(self):
    #    """Optimization to speed up satisfied()"""
    #    self.__friends = [self.other_node(edge) for edge in self.edges] 

